/*
 * socket.h
 *
 *  Created on: 11-May-2011
 *      Author: tan-ce
 */

#ifndef SOCKET_H_
#define SOCKET_H_

#include <cstdlib>
#include <errno.h>

#include <unistd.h>

#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#include <netdb.h>
#include <string>
#include <cstring>
#include <exception>
#include <list>

#ifndef	INADDR_NONE
#define	INADDR_NONE	0xffffffff
#endif	/* INADDR_NONE */

#define __SOCKET_BUFFER_SIZE (64 * 1024) // Buffer size to be used. Affect max
										 // length of lines which can be read

namespace tcetools {

class Socket;
class ListenerSocket;

class SocketError : public std::exception {
public:
	enum SocketType { Generic, Listener };

protected:
	friend class Socket;

	void *sck;
	SocketType sck_type;

	std::string err_msg;
	int unix_errno;
	bool socket_closed;
	bool blocked;

	void init() {
		unix_errno = errno;
		socket_closed = false;
		blocked = (unix_errno == EAGAIN || unix_errno == EWOULDBLOCK);
	}

public:
	SocketError(std::string msg, Socket *sck) {
		err_msg = msg;
		this->sck = (void *) sck;
		sck_type = SocketError::Generic;

		init();
	}

	SocketError(std::string msg, ListenerSocket *sck) {
		err_msg = msg;
		this->sck = (void *) sck;
		sck_type = SocketError::Listener;

		init();
	}

	virtual ~SocketError() throw() {}

	virtual const char* what() const throw() {
		return err_msg.c_str();
	}

	SocketType get_sck_type() { return sck_type; }
	Socket *get_sck() { return (Socket *) sck; }
	ListenerSocket *get_listener() { return (ListenerSocket *) sck; }
	int get_unix_errno() { return unix_errno; }

	bool would_block() {
		return blocked;
	}

	bool sck_closed() { return socket_closed; }

	void append_unix_error() {
		err_msg.append(" (");
		err_msg.append(strerror(errno));
		err_msg.append(")");
	}
}; // class SocketError

////////////////////////////////////////////////////////////////////////////////

class Socket {
protected:
	class chunk {
	public:
		chunk() {
			size = 0;
			buf = 0;
			offset = 0;
		}
		
		~chunk() {
			if (buf) delete [] buf;
		}
		
		void reset(size_t newsize);
		
		void reset() {
			reset(size);
		}
		
		char *buf;
		unsigned int size;
		unsigned int offset;
	};

	char *line_delimiter;
	int line_delimiter_len;
	char input_buf[__SOCKET_BUFFER_SIZE + 1];
	chunk chunk_buf;
	ssize_t input_buf_collected;
	bool socket_closed;

	int sck_id;

	void shift_input_buf(size_t len) {
		input_buf_collected -= len;
		for (size_t i = 0; i < input_buf_collected; i++) {
			input_buf[i] = input_buf[i + len];
		}
	}

	void buffered_read();

	void init();

	Socket() {
		init();
	}

public:
	static void ignore_sigpipe() {
		signal(SIGPIPE, SIG_IGN);
	}

	void blocking_write(const char *buf, size_t len);
	void blocking_write(const char *buf) {
		int len;
		for (len = 0; buf[len]; len++);
		blocking_write(buf, len);
	}

	void blocking_unbuffered_read(char *buf, size_t len);
	void blocking_read(char *buf, size_t len);
	std::string read_line(bool noblock = false);

	void write_line(std::string line) {
		blocking_write(std::string(line + line_delimiter).c_str());
	}

	ssize_t raw_write(const char *buf, size_t len);
	ssize_t raw_read(char *buf, size_t maxlen);
	
	void http_chunk_write(const char *buf, size_t len);
	void http_chunk_read(char *buf, size_t len);
	unsigned int http_chunk_receive(); // Reads in one chunk.

	bool can_read();
	bool sck_closed() { return socket_closed; }

	void sync() {
		fsync(sck_id);
	}

	void close_sock() {
		close(sck_id);
		socket_closed = true;
	}

	void set_noblock();

	void set_line_delimiter(const char *new_delim);

	int get_fd() { return sck_id; }

	Socket(int fd) {
		init();
		sck_id = fd;
	}

	~Socket() {
		close_sock();
		delete [] line_delimiter;
	}
}; // class Socket

////////////////////////////////////////////////////////////////////////////////

class IPSocket : public Socket {
protected:
	IPSocket() {}

	std::string rem_ip;
	unsigned int rem_port;
	std::string local_ip;

public:
	std::string &get_rem_ip() { return rem_ip; }
	unsigned int get_rem_port() { return rem_port; }
	std::string &get_local_ip() { return local_ip; }

	void set_low_latency() {
		int flag = IPTOS_LOWDELAY;
		// Set TOS
		if (setsockopt(sck_id, IPPROTO_IP, IP_TOS, &flag, sizeof(int))) {
			SocketError se("Unable to set IP_TOS", this);
			se.append_unix_error();
			throw se;
		}

		// Disable Nagle
		flag = 1;
		if (setsockopt(sck_id, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(int))) {
			SocketError se("Unable to set TCP_NODELAY", this);
			se.append_unix_error();
			throw se;
		}
	}

	void set_window_size(int wndsize) {
		if (setsockopt(sck_id, SOL_SOCKET, SO_SNDBUF, &wndsize, sizeof(int))) {
			SocketError se("Unable to set SO_SNDBUF", this);
			se.append_unix_error();
			throw se;
		}
		if (setsockopt(sck_id, SOL_SOCKET, SO_RCVBUF, &wndsize, sizeof(int))) {
			SocketError se("Unable to set SO_RCVBUF", this);
			se.append_unix_error();
			throw se;
		}
	}
};

////////////////////////////////////////////////////////////////////////////////

class ClientSocket : public IPSocket {
public:
	ClientSocket(const char *rem_host, const char *rem_service);
}; // class ClientSocket

////////////////////////////////////////////////////////////////////////////////

class ServerSocket;

class ListenerSocket {
protected:
	int sck_id;
public:
	ListenerSocket(const char *service, int qlen);
	ServerSocket *accept_connection();

	// conn is set in the child process, and NULL in the parent
	pid_t accept_connection_and_fork(ServerSocket * &ss);

	int get_fd() { return sck_id; }

	~ListenerSocket() { close(sck_id); }
};

////////////////////////////////////////////////////////////////////////////////

class ServerSocket : public IPSocket {
protected:
	friend class ListenerSocket;
	ServerSocket(int sck_id) { this->sck_id = sck_id; }
};

////////////////////////////////////////////////////////////////////////////////

} // namespace tce_tools

#endif /* SOCKET_H_ */
